package ioc;

import anno.ComponentScan;
import anno.Component;
import anno.Scope;
import anno.Autowired;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

public class ApplicationContext {

    private String packagePath;
    private ClassLoader classLoader;

    private final List<BeanPostProcessor> beanPostProcessors = new CopyOnWriteArrayList<>();
    private final Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();
    private final Map<String, Object> singletonBeanMap = new HashMap<>();
    private final String SINGLETON = "singleton";

    public ApplicationContext(Class clazz) {
        scan(clazz);
        initSingleton();
    }

    private void initSingleton() {
        beanDefinitionMap.forEach((beanName, beanDefinition) -> {
            if (beanDefinition.getScope() == "singleton") {
                doCreateBean(beanDefinition);
            }
        });
    }


    private void scan(Class clazz) {
        if (!clazz.isAnnotationPresent(ComponentScan.class)) {
            throw new NullPointerException("必须传入应该扫描的类");
        }
        ComponentScan componentScan = (ComponentScan) clazz.getAnnotation(ComponentScan.class);
        String packagePath = componentScan.value();
        if (null == componentScan.value() || "".equals(componentScan.value())) {
            packagePath = clazz.getPackage().getName();
        }
        this.classLoader = ApplicationContext.class.getClassLoader();
        this.packagePath = packagePath.replace(".", "/");
        System.out.println("packagePath:" + this.packagePath);
        URL resource = classLoader.getResource(this.packagePath);
        File file = new File(resource.getFile());
        List<Class> beanClassList = new ArrayList<>();
        getClassesFromFile(file, beanClassList);
        System.out.println(beanClassList.size());
        for (Class aClass : beanClassList) {
            if (aClass.isAnnotationPresent(Component.class)) {
                Component component = (Component) aClass.getAnnotation(Component.class);
                String beanName = component.value();
                if (null == component.value() || "".equals(component.value())) {
                    beanName = aClass.getSimpleName();
                }
                BeanDefinition beanDefinition = new BeanDefinition();
                beanDefinition.setBeanClassName(aClass.getName());
                beanDefinition.setClazz(aClass);
                beanDefinition.setBeanName(beanName);
                if (aClass.isAnnotationPresent(Scope.class)) {
                    Scope scope = (Scope) aClass.getAnnotation(Scope.class);
                    beanDefinition.setScope(scope.value());
                } else {
                    beanDefinition.setScope(SINGLETON);
                }
                beanDefinitionMap.put(beanName, beanDefinition);
                if (BeanPostProcessor.class.isAssignableFrom(aClass)) {
                    BeanPostProcessor beanPostProcessor = (BeanPostProcessor) getBean(beanName, aClass);
                    beanPostProcessors.add(beanPostProcessor);
                    singletonBeanMap.put(beanName, beanPostProcessor);
                }
            }
        }
    }

    public <T> T getBean(String beanName, Class<T> tClass) {
        if (singletonBeanMap.containsKey(beanName)) {
            if (tClass.isAssignableFrom(beanDefinitionMap.get(beanName).getClazz())) {
                return (T) singletonBeanMap.get(beanName);
            } else {
                return null;
            }
        }
        if (beanDefinitionMap.containsKey(beanName)) {
            BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
            if (tClass.isAssignableFrom(beanDefinition.getClazz())) {
                T instance = doCreateBean(beanDefinition);
                if (SINGLETON.equals(beanDefinition.getScope())) {
                    singletonBeanMap.put(beanName, instance);
                }
                return instance;
            }
        }
        return null;
    }

    private <T> T doCreateBean(BeanDefinition beanDefinition) {
        Class clazz = beanDefinition.getClazz();
        try {
            Constructor constructor = clazz.getDeclaredConstructor();
            Object instance = constructor.newInstance();
            Field[] fields = clazz.getDeclaredFields();
            //bean实例之前
            for (BeanPostProcessor beanPostProcessor : beanPostProcessors) {
                beanPostProcessor.postProcessBeforeInitialization(instance, beanDefinition.getBeanName());
            }
            for (Field field : fields) {
                if (field.isAnnotationPresent(Autowired.class)) {
                    field.setAccessible(true);
                    field.set(instance, getBean(field.getName(), field.getType()));
                }
            }
            //bean实例化后
            for (BeanPostProcessor beanPostProcessor : beanPostProcessors) {
                beanPostProcessor.postProcessAfterInitialization(instance, beanDefinition.getBeanName());
            }

            return (T) instance;
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }


    private List<Class> getClassesFromFile(File file, List<Class> beanClassList) {
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            for (File file1 : files) {
                if (file1.isDirectory()) {
                    getClassesFromFile(file1, beanClassList);
                } else {
                    addClassToClassList(file1, beanClassList);
                }
            }
        } else {
            addClassToClassList(file, beanClassList);
        }
        return beanClassList;
    }

    private void addClassToClassList(File file, List<Class> beanClassList) {
        String filename = file.getAbsolutePath();
        String path = packagePath.replace("/", "\\");
        if (filename.endsWith(".class") && filename.lastIndexOf(path) >= 0) {
            String className = filename.substring(filename.lastIndexOf(path), filename.indexOf(".class"));
            className = className.replace("\\", ".");
            System.out.println("className = [" + className + "]");
            try {
                Class<?> clazz = classLoader.loadClass(className);
                beanClassList.add(clazz);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }


}
